# -*- coding: utf-8 -*-
"""
A one-stop build and slimming tool that integrates Nuitka compilation and
post-build file slimming into a single GUI application.
"""

import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import subprocess
import threading
import os
from pathlib import Path
from typing import List
import queue

# --- Embedded Configurations ---

# Configuration from white_files.json
WHITE_DICT = {
    "python": [
        "api-ms-win-core",
        "base_library.zip",
        ".tcl",
        "tclIndex",
        "MSVCP140.dll",
        "cacert.pem",
        "cp936.enc",
        "__init__",
        "python.exe",
        "pythonw.exe",
        "VCRUNTIME140_1.dll",
    ],
    "matplotlib": ["matplotlibrc", ".load-order", "matplotlib.svg"],
    "request": ["msgcat-1.6.1.tm"],
    "plotly": [
        "plotly.json",
        "plotly.min.js",
        "package_data\\templates",
        "favicon.ico",
    ],
    "kaleido": ["kaleido\\executable"],
    "pyecharts": ["pyecharts"],
    "pyqtwebengine": [
        "QtWebEngineProcess.exe",
        "icudtl.dat",
        "qtwebengine_devtools_resources.pak",
        "qtwebengine_resources",
        "qt.conf",
    ],
    "streamlit": ["streamlit\\static"],
    "trame_vtk": ["static_viewer.html"],
    "python-docx": ["docx\\templates"],
    "python-pptx": ["pptx\\templates"],
    "setuptools": ["setuptools\\_vendor"],
}
WHITE_FILE_TYPE = [".pyi", ".py", ".pyc", ".pth", "._pth"]

# Configuration from build.bat
NUITKA_CONFIG = {
    "VENV_PYTHON": ".\\.venv\\Scripts\\python.exe",
    "MAIN_SCRIPT": "main.py",
    "OUTPUT_DIR": "dist\\nuitka_output",
    "OUTPUT_FILENAME": "亮亮编译命令生成工具",
    "ICON_FILE": "logo.ico",
}


class FileRemove:
    """File removal and utility functions."""

    @staticmethod
    def get_file_paths(directory: Path, only_binary: bool = True) -> List[Path]:
        file_list = []
        patterns = ("*.pyd", "*.dll") if only_binary else ("**/*.*",)
        for pattern in patterns:
            files_paths = directory.glob(
                pattern if pattern.startswith("**") else "**/" + pattern
            )
            for path in files_paths:
                if path.is_file():
                    file_list.append(path)
        return file_list

    @staticmethod
    def is_file_in_use(file_name: Path) -> bool:
        try:
            with open(str(file_name), "r+", encoding="utf-8"):
                return False
        except (OSError, PermissionError):
            return True
        except Exception:
            return True

    @staticmethod
    def move_file_to_folder(file_old: Path, file_new: Path) -> None:
        if not file_new.parent.exists():
            os.makedirs(file_new.parent, exist_ok=True)
        try:
            file_old.rename(file_new)
        except Exception:
            pass

    @staticmethod
    def check_file(white_list: List[str], file: Path) -> bool:
        file_str = str(file)
        if any(data in file_str for data in white_list):
            return False
        if file.suffix in WHITE_FILE_TYPE:
            return False
        return True


class BuildToolApp(tk.Tk):
    """Main application GUI."""

    def __init__(self):
        super().__init__()
        self.title("一站式编译瘦身工具 (v2.3)")
        self.geometry("800x600")
        self.protocol("WM_DELETE_WINDOW", self.quit)

        self.build_dir = None
        self.log_queue = queue.Queue()
        self.safe_mode_var = tk.BooleanVar(value=True)
        self.slimming_in_progress = False
        self.create_widgets()
        self.process_log_queue()

    def create_widgets(self):
        main_frame = ttk.Frame(self, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)

        controls_frame = ttk.LabelFrame(main_frame, text="控制面板", padding="10")
        controls_frame.pack(fill=tk.X, pady=5)

        self.build_button = ttk.Button(
            controls_frame, text="1. 开始编译", command=self.start_build
        )
        self.build_button.pack(side=tk.LEFT, padx=5)

        self.open_app_button = ttk.Button(
            controls_frame,
            text="2. 打开程序",
            command=self.open_application,
            state=tk.DISABLED,
        )
        self.open_app_button.pack(side=tk.LEFT, padx=5)

        self.confirm_button = ttk.Button(
            controls_frame,
            text="3. 我已手动测试",
            command=self.confirm_tested,
            state=tk.DISABLED,
        )
        self.confirm_button.pack(side=tk.LEFT, padx=5)

        self.slim_button = ttk.Button(
            controls_frame,
            text="4. 开始瘦身",
            command=self.start_slim,
            state=tk.DISABLED,
        )
        self.slim_button.pack(side=tk.LEFT, padx=5)

        self.mode_checkbutton = ttk.Checkbutton(
            controls_frame,
            text="安全模式 (仅处理 .pyd/.dll)",
            variable=self.safe_mode_var,
            onvalue=True,
            offvalue=False,
        )
        self.mode_checkbutton.pack(side=tk.LEFT, padx=10, pady=5)

        log_frame = ttk.LabelFrame(main_frame, text="日志输出", padding="10")
        log_frame.pack(fill=tk.BOTH, expand=True, pady=5)

        self.log_area = scrolledtext.ScrolledText(
            log_frame, wrap=tk.WORD, state=tk.DISABLED
        )
        self.log_area.pack(fill=tk.BOTH, expand=True)

    def process_log_queue(self):
        """Process up to 100 messages from the log queue to prevent UI freeze."""
        try:
            # Batch insert messages
            messages_to_insert = []
            for _ in range(100):
                if self.log_queue.empty():
                    break
                messages_to_insert.append(self.log_queue.get_nowait())

            if messages_to_insert:
                self.log_area.config(state=tk.NORMAL)
                for message in messages_to_insert:
                    self.log_area.insert(tk.END, message)
                self.log_area.see(tk.END)  # Scroll only once after batch insert
                self.log_area.config(state=tk.DISABLED)
        except queue.Empty:
            pass  # This is expected
        finally:
            self.after(100, self.process_log_queue)

    def set_button_states(
        self, build="normal", open_app="disabled", confirm="disabled", slim="disabled"
    ):
        self.build_button.config(state=build)
        self.open_app_button.config(state=open_app)
        self.confirm_button.config(state=confirm)
        self.slim_button.config(state=slim)

    def start_build(self):
        self.set_button_states(
            build="disabled", open_app="disabled", confirm="disabled", slim="disabled"
        )
        self.log_area.config(state=tk.DISABLED)
        self.log_queue.put("--- 开始 Nuitka 编译 ---\n")
        threading.Thread(target=self._build_thread, daemon=True).start()

    def _find_build_dir(self) -> Path | None:
        """Finds the actual .dist directory created by Nuitka."""
        base_dir = Path(NUITKA_CONFIG["OUTPUT_DIR"])
        exe_name = NUITKA_CONFIG["OUTPUT_FILENAME"] + ".exe"
        if not base_dir.exists():
            return None
        for path in base_dir.iterdir():
            if path.is_dir() and path.name.endswith(".dist"):
                if (path / exe_name).exists():
                    return path
        return None

    def _build_thread(self):
        try:
            if not Path(NUITKA_CONFIG["VENV_PYTHON"]).exists():
                self.log_queue.put(
                    f"[ERROR] Python virtual environment not found at: {NUITKA_CONFIG['VENV_PYTHON']}\n"
                )
                self.log_queue.put("编译失败。\n")
                self.set_button_states(build="normal")
                return

            command = [
                NUITKA_CONFIG["VENV_PYTHON"],
                "-m",
                "nuitka",
                "--output-dir=" + NUITKA_CONFIG["OUTPUT_DIR"],
                "--output-filename=" + NUITKA_CONFIG["OUTPUT_FILENAME"],
                NUITKA_CONFIG["MAIN_SCRIPT"],
                "--standalone",
                "--windows-console-mode=disable",
                "--mingw64",
                "--lto=yes",
                "--enable-plugin=pyside6",
                f"--jobs={os.cpu_count()}",
                "--windows-icon-from-ico=" + NUITKA_CONFIG["ICON_FILE"],
                "--include-data-file=STXingkai.ttf=STXingkai.ttf",
                "--include-data-file=logo.ico=logo.ico",
                "--nofollow-import-to=PySide6.QtTest",
                "--nofollow-import-to=PySide6.QtQuick",
                "--nofollow-import-to=PySide6.QtQml",
                "--nofollow-import-to=PySide6.QtWebEngineWidgets",
                "--nofollow-import-to=PySide6.QtWebEngineCore",
                "--nofollow-import-to=PySide6.Qt3DCore",
                "--nofollow-import-to=PySide6.Qt3DRender",
                "--nofollow-import-to=PySide6.Qt3DInput",
                "--nofollow-import-to=PySide6.Qt3DLogic",
                "--nofollow-import-to=PySide6.Qt3DExtras",
                "--nofollow-import-to=PySide6.QtCharts",
                "--nofollow-import-to=PySide6.QtDataVisualization",
                "--nofollow-import-to=PySide6.QtNetworkAuth",
                "--show-progress",
                "--show-scons",
            ]

            env = os.environ.copy()
            env["PYTHONIOENCODING"] = "utf-8"

            process = subprocess.Popen(
                command,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                encoding="utf-8",
                errors="replace",
                bufsize=1,
                creationflags=subprocess.CREATE_NO_WINDOW,
                env=env,
            )

            for line in iter(process.stdout.readline, ""):
                self.log_queue.put(line)
            process.stdout.close()
            return_code = process.wait()

            if return_code == 0:
                self.log_queue.put("\n--- 编译成功 ---\n")
                self.build_dir = self._find_build_dir()
                if self.build_dir:
                    self.log_queue.put(f"成功定位输出目录: {self.build_dir}\n")
                    self.log_queue.put(
                        "请点击“打开程序”按钮进行测试，或直接手动测试。\n"
                    )
                    self.set_button_states(
                        build="disabled",
                        open_app="normal",
                        confirm="normal",
                        slim="disabled",
                    )
                else:
                    self.log_queue.put("[ERROR] 编译声称成功，但无法找到输出目录！\n")
                    self.set_button_states(build="normal")
            else:
                self.log_queue.put(f"\n--- 编译失败 (Exit Code: {return_code}) ---\n")
                self.set_button_states(build="normal")
        except Exception as e:
            self.log_queue.put(f"\n--- 发生严重错误 ---\n{e}\n")
            self.set_button_states(build="normal")

    def open_application(self):
        if not self.build_dir:
            messagebox.showerror("错误", "未找到编译输出目录。")
            return

        exe_path = self.build_dir / (NUITKA_CONFIG["OUTPUT_FILENAME"] + ".exe")
        if not exe_path.exists():
            messagebox.showerror("错误", f"找不到应用程序: {exe_path}")
            return

        try:
            os.startfile(exe_path)
            self.log_queue.put(f"正在尝试打开应用程序: {exe_path}\n")
        except Exception as e:
            self.log_queue.put(f"[ERROR] 打开应用程序失败: {e}\n")
            messagebox.showerror("错误", f"打开应用程序失败: {e}")

    def confirm_tested(self):
        self.set_button_states(
            build="disabled", open_app="disabled", confirm="disabled", slim="normal"
        )
        self.log_queue.put("\n--- 用户确认测试完成 ---\n")

    def start_slim(self):
        if self.slimming_in_progress:
            self.log_queue.put("[WARN] 瘦身已在进行中，请勿重复操作。\n")
            return

        if not self.build_dir or not self.build_dir.exists():
            messagebox.showerror("错误", "未找到编译输出目录，请先成功执行编译。")
            return

        self.slimming_in_progress = True
        self.set_button_states(
            build="disabled", open_app="disabled", confirm="disabled", slim="disabled"
        )
        self.log_queue.put(f"\n--- 开始瘦身处理: {self.build_dir} ---\n")
        threading.Thread(target=self._slim_thread, daemon=True).start()

    def _slim_thread(self):
        try:
            is_safe_mode = self.safe_mode_var.get()
            self.log_queue.put(
                f"瘦身模式: {'安全模式' if is_safe_mode else '极限模式'}\n"
            )

            white_list = [item for sublist in WHITE_DICT.values() for item in sublist]
            file_dir_new = Path(str(self.build_dir) + "_new")
            file_list = FileRemove.get_file_paths(
                self.build_dir, only_binary=is_safe_mode
            )
            file_move_lst = []
            file_trans_lst = self.build_dir.parent.joinpath(
                f"{self.build_dir.name}_文件移动清单.txt"
            )
            file_count = len(file_list)

            if file_count == 0:
                self.log_queue.put("未找到可处理的文件 (pyd, dll)。\n")
                return

            for idx, filename in enumerate(file_list):
                if FileRemove.check_file(
                    white_list, filename
                ) and not FileRemove.is_file_in_use(filename):
                    relative_path = filename.relative_to(self.build_dir)
                    filename_new = file_dir_new.joinpath(relative_path)
                    FileRemove.move_file_to_folder(filename, filename_new)
                    file_move_lst.append(str(filename))

            self.log_queue.put(
                f"处理了 {file_count} 个文件，移动了 {len(file_move_lst)} 个文件。\n"
            )

            if file_trans_lst.exists():
                os.remove(str(file_trans_lst))
            with open(file_trans_lst, "w", encoding="utf-8") as file:
                file.write("\n".join(file_move_lst))

            self.log_queue.put(
                f"\n--- 瘦身完成 ---\n瘦身后的文件位于: {file_dir_new}\n"
            )
            self.log_queue.put(f"移动清单: {file_trans_lst}\n")

        except Exception as e:
            self.log_queue.put(f"\n--- 瘦身过程中发生错误 ---\n{e}\n")
        finally:
            self.set_button_states(
                build="normal", open_app="disabled", confirm="disabled", slim="normal"
            )
            self.slimming_in_progress = False


if __name__ == "__main__":
    try:
        from ctypes import windll

        windll.shcore.SetProcessDpiAwareness(1)
    except Exception:
        pass

    app = BuildToolApp()
    app.mainloop()
